Pourquoi pas débruiter un chat ? =^.^=¶
L'objectif, via ce projet est de pouvoir générer, en sortie, des images de chat en retirant le bruit présent.
On va donc tout d'abord construire un Autoencoder pour produire un Denoising Autoencoder.
Puisque les résultats de celui-ci restent limités. On va alors construire un réseau U-Net pour obtenir générer des images plus proches de celles en entrée.
Enfin on utilisera Stable Diffusion XL (SDXL) pour essayer de produire une image un peu plus détaillé (pour atténuer le flou obtenu lors de la génération).
Dataset¶
Le jeu de données provient de Kaggle.
Il contient plus de 9 000 images de chats et, pour chaque image, un fichier .cat avec la position des caractéristiques importantes du chat (yeux, nez, oreilles, etc.).
Les données d'annotation sont stockées dans un fichier portant le nom de l'image correspondante plus « cat » à la fin. Il y a un fichier d'annotation pour chaque image de chat. Pour chaque fichier d'annotation, les données d'annotation sont stockées dans l'ordre suivant :
- Nombre de points (9 par défaut)
- Œil gauche
- Œil droit
- Bouche
- Oreille gauche-1
- Oreille gauche-2
- Oreille gauche-3
- Oreille droite-1
- Oreille droite-2
- Oreille droite-3
Import the libraries¶
import kagglehub
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from PIL import Image
import shutil
import os
import torch
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
from sklearn.metrics import accuracy_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
C:\Users\maxka\anaconda3\envs\dl_clean\lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
print("CUDA dispo ?", torch.cuda.is_available())
print("Nom du GPU :", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "Pas de GPU")
CUDA dispo ? True Nom du GPU : NVIDIA GeForce RTX 4080 SUPER
Download the dataset¶
path = kagglehub.dataset_download("crawford/cat-dataset")
print("Path to dataset files:", path)
Warning: Looks like you're using an outdated `kagglehub` version, please consider updating (latest version: 0.3.12) Path to dataset files: C:\Users\maxka\.cache\kagglehub\datasets\crawford\cat-dataset\versions\2
Inspect the dataset¶
List the files in the dataset directory¶
print("Files in the dataset directory:")
for folder in os.listdir(path):
print(f"Folder {folder} has {len(os.listdir(os.path.join(path, folder)))} files ")
if 'data' not in os.listdir():
os.mkdir("data")
Files in the dataset directory: Folder cats has 0 files Folder CAT_00 has 0 files Folder CAT_01 has 0 files Folder CAT_02 has 0 files Folder CAT_03 has 0 files Folder CAT_04 has 0 files Folder CAT_05 has 0 files Folder CAT_06 has 0 files
Move the images and the .cat files in different folders¶
if 'images' not in os.listdir("data"):
os.mkdir("data/images/no_category")
if 'cat_files' not in os.listdir("data"):
os.mkdir("data/cat_files")
Move the images and the .cat files in different folders¶
for folder in os.listdir(path):
for file in os.listdir(os.path.join(path, folder)):
if file.endswith(".cat"):
shutil.move(os.path.join(path, folder, file), os.path.join("data/cat_files", file))
else:
shutil.move(os.path.join(path, folder, file), os.path.join("data/images/no_category", file))
## List the files in the images directory
print(f"The images directory has {len(os.listdir('data/images/no_category'))} files")
## List the files in the cat_files directory
print(f"The cat_files directory has {len(os.listdir('data/cat_files'))} files")
The images directory has 10001 files The cat_files directory has 9993 files
Data Loading¶
img_height = 150
img_width = 150
cat_folder = "data/images/no_category"
images = []
for filename in os.listdir(cat_folder):
if filename.endswith(".jpg") or filename.endswith(".png"):
img_path = os.path.join(cat_folder, filename)
img = Image.open(img_path)
img = img.resize((img_height, img_width))
images.append(np.array(img))
images = np.array(images)
# normalisation des pixels entre 0 et 1
images = images.astype("float32") / 255.0
print(f"images shape: {images.shape}")
images shape: (9993, 150, 150, 3)
plt.figure(figsize=(10, 10))
for i in range(9):
ax = plt.subplot(3, 3, i + 1)
plt.imshow(images[i])
plt.axis("off")
plt.show()
Train/Test split¶
x_train, x_test = train_test_split(images, test_size=0.2, random_state=42)
print(f"x_train shape: {x_train.shape}")
print(f"x_test shape: {x_test.shape}")
x_train shape: (7994, 150, 150, 3) x_test shape: (1999, 150, 150, 3)
Data augmentation¶
Ici nous transformons les images en 150x150 puis nous les normalisons. Nous augmentons ensuite les images en les retournant horizontalement et en les faisant pivoter afin de créer plus de variations dans les données d'entraînement.
IMG_SIZE = 150
# resize + rescale (normalisation en [0,1])
resize_and_rescale = transforms.Compose([
transforms.Resize((IMG_SIZE, IMG_SIZE)),
transforms.ToTensor() # convertit en float tensor et divise par 255 automatiquement
])
# augmentation des données : flip horizontal+vertical + rotation aléatoire
data_augmentation = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.5),
transforms.RandomRotation(20), # ici on met 20 degrés environ
])
Un autoencodeur convolutionnel :
- Encodeur (compression) :
3 couches Conv2D + MaxPooling2D réduisent progressivement la taille des images
- Decodeur (reconstruction) :
3 couches Conv2D + UpSampling2D agrandissent progressivement pour recréer l’image originale
La couche finale Conv2D avec activation sigmoid pour produit une image RGB normalisée
class Autoencoder(nn.Module):
def __init__(self):
super(Autoencoder, self).__init__()
# Encodeur
self.encoder = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, padding=1), # padding='same'
nn.ReLU(),
nn.MaxPool2d(2, stride=2, padding=1),
nn.Conv2d(16, 8, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2, padding=1),
nn.Conv2d(8, 8, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2, padding=1)
)
# Décodeur
self.decoder = nn.Sequential(
nn.Conv2d(8, 8, kernel_size=3, padding=1),
nn.ReLU(),
nn.Upsample(scale_factor=2, mode='nearest'),
nn.Conv2d(8, 8, kernel_size=3, padding=1),
nn.ReLU(),
nn.Upsample(scale_factor=2, mode='nearest'),
nn.Conv2d(8, 16, kernel_size=3), # pas de padding ici, pour correspondre au code du tf
nn.ReLU(),
nn.Upsample(scale_factor=2, mode='nearest'),
nn.Conv2d(16, 3, kernel_size=3, padding=1),
nn.Sigmoid() # sortie entre 0 et 1
)
def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
# on resize la sortie finale à (150,150)
x = F.interpolate(x, size=(150,150), mode='bilinear', align_corners=False)
return x
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
autoencoder = Autoencoder().to(device)
# Optimiseur + loss
optimizer = torch.optim.Adam(autoencoder.parameters()) # ou NAdam, à voir
criterion = nn.MSELoss()
# input tensor doit avoir shape [batch_size, 3, 150, 150]
# (PyTorch utilise channels_first)
dummy_input = torch.randn(1, 3, 150, 150).to(device)
output = autoencoder(dummy_input)
print(output.shape) # -> torch.Size([1, 3, 150, 150])
torch.Size([1, 3, 150, 150])
Préparer les Dataloaders¶
Les Dataloaders sont des objets qui permettent de charger les données en mini-batchs pour l'entraînement du modèle. Ils sont créés à partir des ensembles d'entraînement et de validation, en appliquant les transformations définies précédemment.
batch_size = 128
x_train = torch.tensor(x_train).permute(0, 3, 1, 2).float()
x_test = torch.tensor(x_test).permute(0, 3, 1, 2).float()
train_dataset = TensorDataset(x_train, x_train) # autoencodeur : input = target
test_dataset = TensorDataset(x_test, x_test)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
epochs = 150
for epoch in range(epochs):
autoencoder.train()
train_loss = 0
for inputs, targets in train_loader:
inputs = inputs.to('cuda')
targets = targets.to('cuda')
optimizer.zero_grad()
outputs = autoencoder(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
train_loss += loss.item() * inputs.size(0)
train_loss /= len(train_loader.dataset)
print(f"Epoch {epoch+1}/{epochs}, Loss: {train_loss:.6f}")
Epoch 1/150, Loss: 0.057349 Epoch 2/150, Loss: 0.018815 Epoch 3/150, Loss: 0.016387 Epoch 4/150, Loss: 0.014955 Epoch 5/150, Loss: 0.014164 Epoch 6/150, Loss: 0.013260 Epoch 7/150, Loss: 0.011571 Epoch 8/150, Loss: 0.010900 Epoch 9/150, Loss: 0.010555 Epoch 10/150, Loss: 0.010372 Epoch 11/150, Loss: 0.010184 Epoch 12/150, Loss: 0.010060 Epoch 13/150, Loss: 0.009998 Epoch 14/150, Loss: 0.009840 Epoch 15/150, Loss: 0.009793 Epoch 16/150, Loss: 0.009748 Epoch 17/150, Loss: 0.009594 Epoch 18/150, Loss: 0.009536 Epoch 19/150, Loss: 0.009467 Epoch 20/150, Loss: 0.009649 Epoch 21/150, Loss: 0.009349 Epoch 22/150, Loss: 0.009411 Epoch 23/150, Loss: 0.009321 Epoch 24/150, Loss: 0.009267 Epoch 25/150, Loss: 0.009201 Epoch 26/150, Loss: 0.009174 Epoch 27/150, Loss: 0.009135 Epoch 28/150, Loss: 0.009122 Epoch 29/150, Loss: 0.009072 Epoch 30/150, Loss: 0.009091 Epoch 31/150, Loss: 0.009027 Epoch 32/150, Loss: 0.008982 Epoch 33/150, Loss: 0.008998 Epoch 34/150, Loss: 0.008942 Epoch 35/150, Loss: 0.008945 Epoch 36/150, Loss: 0.008927 Epoch 37/150, Loss: 0.008887 Epoch 38/150, Loss: 0.008853 Epoch 39/150, Loss: 0.008822 Epoch 40/150, Loss: 0.008812 Epoch 41/150, Loss: 0.008814 Epoch 42/150, Loss: 0.008816 Epoch 43/150, Loss: 0.008854 Epoch 44/150, Loss: 0.008716 Epoch 45/150, Loss: 0.008721 Epoch 46/150, Loss: 0.008727 Epoch 47/150, Loss: 0.008743 Epoch 48/150, Loss: 0.008689 Epoch 49/150, Loss: 0.008717 Epoch 50/150, Loss: 0.008696 Epoch 51/150, Loss: 0.008617 Epoch 52/150, Loss: 0.008642 Epoch 53/150, Loss: 0.008604 Epoch 54/150, Loss: 0.008670 Epoch 55/150, Loss: 0.008602 Epoch 56/150, Loss: 0.008568 Epoch 57/150, Loss: 0.008619 Epoch 58/150, Loss: 0.008614 Epoch 59/150, Loss: 0.008534 Epoch 60/150, Loss: 0.008534 Epoch 61/150, Loss: 0.008578 Epoch 62/150, Loss: 0.008543 Epoch 63/150, Loss: 0.008517 Epoch 64/150, Loss: 0.008507 Epoch 65/150, Loss: 0.008485 Epoch 66/150, Loss: 0.008466 Epoch 67/150, Loss: 0.008542 Epoch 68/150, Loss: 0.008449 Epoch 69/150, Loss: 0.008533 Epoch 70/150, Loss: 0.008511 Epoch 71/150, Loss: 0.008400 Epoch 72/150, Loss: 0.008410 Epoch 73/150, Loss: 0.008407 Epoch 74/150, Loss: 0.008426 Epoch 75/150, Loss: 0.008431 Epoch 76/150, Loss: 0.008399 Epoch 77/150, Loss: 0.008376 Epoch 78/150, Loss: 0.008421 Epoch 79/150, Loss: 0.008374 Epoch 80/150, Loss: 0.008345 Epoch 81/150, Loss: 0.008396 Epoch 82/150, Loss: 0.008337 Epoch 83/150, Loss: 0.008356 Epoch 84/150, Loss: 0.008327 Epoch 85/150, Loss: 0.008335 Epoch 86/150, Loss: 0.008310 Epoch 87/150, Loss: 0.008296 Epoch 88/150, Loss: 0.008333 Epoch 89/150, Loss: 0.008319 Epoch 90/150, Loss: 0.008304 Epoch 91/150, Loss: 0.008272 Epoch 92/150, Loss: 0.008308 Epoch 93/150, Loss: 0.008263 Epoch 94/150, Loss: 0.008267 Epoch 95/150, Loss: 0.008225 Epoch 96/150, Loss: 0.008268 Epoch 97/150, Loss: 0.008211 Epoch 98/150, Loss: 0.008211 Epoch 99/150, Loss: 0.008228 Epoch 100/150, Loss: 0.008211 Epoch 101/150, Loss: 0.008226 Epoch 102/150, Loss: 0.008175 Epoch 103/150, Loss: 0.008239 Epoch 104/150, Loss: 0.008214 Epoch 105/150, Loss: 0.008209 Epoch 106/150, Loss: 0.008185 Epoch 107/150, Loss: 0.008130 Epoch 108/150, Loss: 0.008202 Epoch 109/150, Loss: 0.008180 Epoch 110/150, Loss: 0.008150 Epoch 111/150, Loss: 0.008134 Epoch 112/150, Loss: 0.008144 Epoch 113/150, Loss: 0.008127 Epoch 114/150, Loss: 0.008124 Epoch 115/150, Loss: 0.008119 Epoch 116/150, Loss: 0.008182 Epoch 117/150, Loss: 0.008099 Epoch 118/150, Loss: 0.008090 Epoch 119/150, Loss: 0.008123 Epoch 120/150, Loss: 0.008072 Epoch 121/150, Loss: 0.008089 Epoch 122/150, Loss: 0.008120 Epoch 123/150, Loss: 0.008076 Epoch 124/150, Loss: 0.008072 Epoch 125/150, Loss: 0.008100 Epoch 126/150, Loss: 0.008084 Epoch 127/150, Loss: 0.008049 Epoch 128/150, Loss: 0.008033 Epoch 129/150, Loss: 0.008110 Epoch 130/150, Loss: 0.008002 Epoch 131/150, Loss: 0.008065 Epoch 132/150, Loss: 0.008046 Epoch 133/150, Loss: 0.008012 Epoch 134/150, Loss: 0.007999 Epoch 135/150, Loss: 0.008018 Epoch 136/150, Loss: 0.008040 Epoch 137/150, Loss: 0.008014 Epoch 138/150, Loss: 0.008010 Epoch 139/150, Loss: 0.007997 Epoch 140/150, Loss: 0.007981 Epoch 141/150, Loss: 0.008003 Epoch 142/150, Loss: 0.007999 Epoch 143/150, Loss: 0.007997 Epoch 144/150, Loss: 0.007967 Epoch 145/150, Loss: 0.007955 Epoch 146/150, Loss: 0.007993 Epoch 147/150, Loss: 0.007954 Epoch 148/150, Loss: 0.007963 Epoch 149/150, Loss: 0.007992 Epoch 150/150, Loss: 0.007971
Reconstruction visualization¶
Nous visualisons les images originales et reconstruites pour évaluer la performance de l'autoencodeur.
autoencoder.eval() # mode évaluation
# On prend les 10 premières images du test
original_images = x_test[:10].to('cuda')
with torch.no_grad():
reconstructed = autoencoder(original_images)
# On remet sur CPU et on convertit en numpy (channels_last)
original_images = original_images.cpu().permute(0, 2, 3, 1).numpy()
reconstructed = reconstructed.cpu().permute(0, 2, 3, 1).numpy()
plt.figure(figsize=(20, 4))
for i in range(10):
# Image originale
ax = plt.subplot(2, 10, i + 1)
plt.imshow(original_images[i])
plt.axis('off')
# Image reconstruite
ax = plt.subplot(2, 10, i + 1 + 10)
plt.imshow(reconstructed[i])
plt.axis('off')
plt.show()
Comme nous pouvons le voir, l'autoencodeur est capable de reconstruire les images de manière reconnaissable mais très floues. Certains détails sont perdus, mais cela montre que l'autoencodeur a appris à capturer les caractéristiques essentielles des images.
Denoising Autoencoder (DAE)¶
Entraînement avec le même autoencoder mais avec une image bruitée en entrée et comme cible l'image propre en sortie.
noise_factor = 0.2
x_train_noisy = x_train + noise_factor * torch.randn_like(x_train)
x_test_noisy = x_test + noise_factor * torch.randn_like(x_test)
# Clip entre 0 et 1
x_train_noisy = torch.clamp(x_train_noisy, 0., 1.)
x_test_noisy = torch.clamp(x_test_noisy, 0., 1.)
Préparer les DataLoaders avec images bruitées en entrée et images propres en sortie (targets)¶
batch_size = 128
train_dataset = TensorDataset(x_train_noisy, x_train) # input noisy, target clean
test_dataset = TensorDataset(x_test_noisy, x_test)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size)
print(x_train_noisy.shape, x_train.shape)
print(x_train_noisy.dtype, x_train.dtype)
# aucune valeur NaN
print(np.isnan(x_train_noisy).sum())
print(np.isnan(x_train).sum())
torch.Size([7994, 3, 150, 150]) torch.Size([7994, 3, 150, 150]) torch.float32 torch.float32 tensor(0) tensor(0)
Entraîner l'autoencoder comme DAE¶
epochs = 75
autoencoder.train()
for epoch in range(epochs):
train_loss = 0
for noisy_imgs, clean_imgs in train_loader:
noisy_imgs = noisy_imgs.to('cuda')
clean_imgs = clean_imgs.to('cuda')
optimizer.zero_grad()
outputs = autoencoder(noisy_imgs)
loss = criterion(outputs, clean_imgs)
loss.backward()
optimizer.step()
train_loss += loss.item() * noisy_imgs.size(0)
train_loss /= len(train_loader.dataset)
print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}")
Epoch 1/75, Train Loss: 0.009811 Epoch 2/75, Train Loss: 0.008472 Epoch 3/75, Train Loss: 0.008444 Epoch 4/75, Train Loss: 0.008429 Epoch 5/75, Train Loss: 0.008410 Epoch 6/75, Train Loss: 0.008404 Epoch 7/75, Train Loss: 0.008477 Epoch 8/75, Train Loss: 0.008407 Epoch 9/75, Train Loss: 0.008392 Epoch 10/75, Train Loss: 0.008385 Epoch 11/75, Train Loss: 0.008387 Epoch 12/75, Train Loss: 0.008371 Epoch 13/75, Train Loss: 0.008376 Epoch 14/75, Train Loss: 0.008360 Epoch 15/75, Train Loss: 0.008405 Epoch 16/75, Train Loss: 0.008365 Epoch 17/75, Train Loss: 0.008383 Epoch 18/75, Train Loss: 0.008347 Epoch 19/75, Train Loss: 0.008382 Epoch 20/75, Train Loss: 0.008362 Epoch 21/75, Train Loss: 0.008310 Epoch 22/75, Train Loss: 0.008315 Epoch 23/75, Train Loss: 0.008346 Epoch 24/75, Train Loss: 0.008340 Epoch 25/75, Train Loss: 0.008341 Epoch 26/75, Train Loss: 0.008316 Epoch 27/75, Train Loss: 0.008352 Epoch 28/75, Train Loss: 0.008284 Epoch 29/75, Train Loss: 0.008303 Epoch 30/75, Train Loss: 0.008362 Epoch 31/75, Train Loss: 0.008355 Epoch 32/75, Train Loss: 0.008294 Epoch 33/75, Train Loss: 0.008295 Epoch 34/75, Train Loss: 0.008281 Epoch 35/75, Train Loss: 0.008271 Epoch 36/75, Train Loss: 0.008303 Epoch 37/75, Train Loss: 0.008258 Epoch 38/75, Train Loss: 0.008333 Epoch 39/75, Train Loss: 0.008255 Epoch 40/75, Train Loss: 0.008251 Epoch 41/75, Train Loss: 0.008262 Epoch 42/75, Train Loss: 0.008236 Epoch 43/75, Train Loss: 0.008317 Epoch 44/75, Train Loss: 0.008231 Epoch 45/75, Train Loss: 0.008293 Epoch 46/75, Train Loss: 0.008237 Epoch 47/75, Train Loss: 0.008219 Epoch 48/75, Train Loss: 0.008232 Epoch 49/75, Train Loss: 0.008231 Epoch 50/75, Train Loss: 0.008222 Epoch 51/75, Train Loss: 0.008225 Epoch 52/75, Train Loss: 0.008232 Epoch 53/75, Train Loss: 0.008245 Epoch 54/75, Train Loss: 0.008232 Epoch 55/75, Train Loss: 0.008212 Epoch 56/75, Train Loss: 0.008198 Epoch 57/75, Train Loss: 0.008302 Epoch 58/75, Train Loss: 0.008193 Epoch 59/75, Train Loss: 0.008214 Epoch 60/75, Train Loss: 0.008228 Epoch 61/75, Train Loss: 0.008219 Epoch 62/75, Train Loss: 0.008198 Epoch 63/75, Train Loss: 0.008171 Epoch 64/75, Train Loss: 0.008206 Epoch 65/75, Train Loss: 0.008176 Epoch 66/75, Train Loss: 0.008193 Epoch 67/75, Train Loss: 0.008170 Epoch 68/75, Train Loss: 0.008198 Epoch 69/75, Train Loss: 0.008173 Epoch 70/75, Train Loss: 0.008194 Epoch 71/75, Train Loss: 0.008145 Epoch 72/75, Train Loss: 0.008219 Epoch 73/75, Train Loss: 0.008164 Epoch 74/75, Train Loss: 0.008155 Epoch 75/75, Train Loss: 0.008187
num_images = 10
indices = np.arange(num_images) # prend simplement les 10 premiers indices
# Sélection des images bruitées et propres
noisy_imgs = x_test_noisy[indices].to('cuda')
clean_imgs = x_test[indices].to('cuda')
autoencoder.eval()
with torch.no_grad():
reconstructed_imgs = autoencoder(noisy_imgs)
# Passage en numpy et réarrangement des dimensions pour matplotlib (CHW -> HWC)
noisy_imgs = noisy_imgs.cpu().permute(0, 2, 3, 1).numpy()
clean_imgs = clean_imgs.cpu().permute(0, 2, 3, 1).numpy()
reconstructed_imgs = reconstructed_imgs.cpu().permute(0, 2, 3, 1).numpy()
plt.figure(figsize=(20, 6))
for i in range(num_images):
ax = plt.subplot(3, num_images, i + 1)
plt.imshow(noisy_imgs[i])
plt.title("Bruitée")
plt.axis("off")
ax = plt.subplot(3, num_images, i + 1 + num_images)
plt.imshow(clean_imgs[i])
plt.title("Originale")
plt.axis("off")
ax = plt.subplot(3, num_images, i + 1 + 2 * num_images)
plt.imshow(reconstructed_imgs[i])
plt.title("Reconstruite")
plt.axis("off")
plt.tight_layout()
plt.show()
Denoising Autoencoder (DAE) basé sur U-Net¶
Nous allons désormais utiliser un modèle U-Net pour le Denoising Autoencoder. Le U-Net est un type d'architecture de réseau de neurones convolutifs qui est particulièrement efficace pour les tâches de segmentation d'images, mais il peut également être utilisé pour la reconstruction d'images.
class AutoencoderUNet(nn.Module):
def __init__(self):
super().__init__()
self.enc1 = self.block(3, 64)
self.enc2 = self.block(64, 128)
self.enc3 = self.block(128, 256)
self.middle = self.block(256, 512)
self.up3 = self.upsample(512, 256) # middle (512) -> e3 (256)
self.up2 = self.upsample(256, 128) # d3 (256) -> e2 (128)
self.up1 = self.upsample(128, 64) # d2 (128) -> e1 (64)
self.final = nn.Conv2d(64, 3, kernel_size=1)
def block(self, in_ch, out_ch):
return nn.Sequential(
nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(),
nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU()
)
def upsample(self, in_ch, out_ch):
return nn.Sequential(
nn.ConvTranspose2d(in_ch, out_ch, kernel_size=2, stride=2),
nn.ReLU()
)
def forward(self, x):
e1 = self.enc1(x)
e2 = self.enc2(F.max_pool2d(e1, 2))
e3 = self.enc3(F.max_pool2d(e2, 2))
m = self.middle(F.max_pool2d(e3, 2))
d3 = self.up3(m)
if d3.shape != e3.shape:
d3 = F.interpolate(d3, size=e3.shape[2:])
d3 = d3 + e3
d2 = self.up2(d3)
if d2.shape != e2.shape:
d2 = F.interpolate(d2, size=e2.shape[2:])
d2 = d2 + e2
d1 = self.up1(d2)
if d1.shape != e1.shape:
d1 = F.interpolate(d1, size=e1.shape[2:])
d1 = d1 + e1
out = torch.sigmoid(self.final(d1))
return out
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
autoencoder_unet = AutoencoderUNet().to(device)
# Optimiseur + loss
optimizer = torch.optim.Adam(autoencoder_unet.parameters()) # ou NAdam, à voir
criterion = nn.MSELoss()
batch_size=32
train_dataset = TensorDataset(x_train_noisy, x_train) # input noisy, target clean
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=False,num_workers=0 )
epochs = 25
autoencoder_unet.train()
for epoch in range(epochs):
train_loss = 0
for noisy_imgs, clean_imgs in train_loader:
noisy_imgs = noisy_imgs.to('cuda')
clean_imgs = clean_imgs.to('cuda')
optimizer.zero_grad()
outputs = autoencoder_unet(noisy_imgs)
loss = criterion(outputs, clean_imgs)
loss.backward()
optimizer.step()
train_loss += loss.item() * noisy_imgs.size(0)
train_loss /= len(train_loader.dataset)
print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.6f}")
Epoch 1/25, Train Loss: 0.004538 Epoch 2/25, Train Loss: 0.003361 Epoch 3/25, Train Loss: 0.003143 Epoch 4/25, Train Loss: 0.003004 Epoch 5/25, Train Loss: 0.002906 Epoch 6/25, Train Loss: 0.002830 Epoch 7/25, Train Loss: 0.002720 Epoch 8/25, Train Loss: 0.002630 Epoch 9/25, Train Loss: 0.002580 Epoch 10/25, Train Loss: 0.002514 Epoch 11/25, Train Loss: 0.002442 Epoch 12/25, Train Loss: 0.002395 Epoch 13/25, Train Loss: 0.002354 Epoch 14/25, Train Loss: 0.002318 Epoch 15/25, Train Loss: 0.002294 Epoch 16/25, Train Loss: 0.002270 Epoch 17/25, Train Loss: 0.002248 Epoch 18/25, Train Loss: 0.002223 Epoch 19/25, Train Loss: 0.002203 Epoch 20/25, Train Loss: 0.002181 Epoch 21/25, Train Loss: 0.002169 Epoch 22/25, Train Loss: 0.002157 Epoch 23/25, Train Loss: 0.002145 Epoch 24/25, Train Loss: 0.002134 Epoch 25/25, Train Loss: 0.002125
# Passer le modèle en mode évaluation
autoencoder_unet.eval()
# On prend les 10 premières images du test
original_images = x_test[:10].to('cuda')
# Prédictions
with torch.no_grad():
reconstructed_imgs_unet = autoencoder_unet(original_images)
# On remet sur CPU et on convertit en numpy (channels_last)
original_images = original_images.cpu().permute(0, 2, 3, 1).numpy()
reconstructed_imgs_unet = reconstructed_imgs_unet.cpu().permute(0, 2, 3, 1).numpy()
plt.figure(figsize=(20, 4))
for i in range(10):
# Image originale
ax = plt.subplot(2, 10, i + 1)
plt.imshow(original_images[i])
plt.axis('off')
# Image reconstruite
ax = plt.subplot(2, 10, i + 1 + 10)
plt.imshow(reconstructed_imgs_unet[i])
plt.axis('off')
plt.show()
Nous voyons que ce modèle est capable de reconstruire les images de manière plus nette et précise que l'autoencodeur précédent. Les détails sont mieux préservés, ce qui montre que le U-Net est plus adapté pour cette tâche de reconstruction d'images. Cependant, il manque toujours des détails (pas mal de flou).
Open-Source Model : SDXL¶
SDXL est un modèle de génération d’images à partir de texte (text-to-image) basé sur les diffusion models.
Le modèle reçoit une entrée avec bruit et va distinguer les images au sein de ce bruit, selon le coefficient de guidage (guidance) à partir d'un prompt, et d'un paramètre strength indiquant à quel point on veut rester proche de l'entrée, améliorant l'image étape par étape.
import torch
print("CUDA dispo ?", torch.cuda.is_available())
print("Nom du GPU :", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "Pas de GPU")
CUDA dispo ? True Nom du GPU : NVIDIA GeForce RTX 4080 SUPER
import torch
from diffusers import StableDiffusionXLImg2ImgPipeline
from diffusers.utils import load_image
pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-refiner-1.0", torch_dtype=torch.float16, variant="fp16", use_safetensors=True
)
pipe = pipe.to("cuda")
#url = "https://huggingface.co/datasets/patrickvonplaten/images/resolve/main/aa_xl/000000009.png"
init_image = x_train_noisy[0] # Use the first noisy image as the initial image
C:\Users\maxka\anaconda3\envs\dl_clean\lib\site-packages\h5py\__init__.py:36: UserWarning: h5py is running against HDF5 1.14.6 when it was built against 1.14.5, this may cause problems
_warn(("h5py is running against HDF5 {0} when it was built against {1}, "
Loading pipeline components...: 100%|████████████████████████████████████████████████████| 5/5 [00:00<00:00, 10.06it/s]
Prompt¶
Cela permet à SDXL de reconstruire l'image guidée sémantiquement, par exemple en restaurant les traits d’un chat même si l’image de base est bruitée.
prompt = "a clean photo of a cat"
strength = 0.02 # Contrôle combien on se rapproche du prompt (1 = complètement nouveau, 0 = identique)
result = pipe(prompt=prompt, image=init_image, strength=strength, guidance_scale=7.5) # guidance_scale, à quel point on suit le prompt, plus c'est élevé, plus c'est strict
output_image = result.images[0]
100%|████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 5.13it/s]
plt.figure(figsize=(15,5))
plt.subplot(1,3,1)
plt.title("Image bruitée")
plt.imshow(np.transpose(x_train_noisy[0], (1, 2, 0)))
plt.subplot(1,3,2)
plt.title("Image originale")
plt.imshow(np.transpose(x_train[0], (1, 2, 0)))
plt.subplot(1,3,3)
plt.title("SDXL")
plt.imshow(output_image)
plt.show()
Nous pouvons voir ici que le modèle n'est pas très performant pour les images bruitées. Le coeur du problème est que le modèle SDXL a été entraîné sur des images de haute qualité et n'est pas conçu pour gérer des images bruitées. Il est donc important de prétraiter les images (par exemple avec U-Net) avant de les passer au modèle et de modifier les paramètres de génération pour obtenir de meilleurs résultats.
Convertir un tensor PyTorch (C, H, W) → PIL Image (RGB)¶
def tensor_to_pil(tensor):
"""
PyTorch tensor CHW (valeurs entre 0-1) -> PIL Image
"""
tensor = tensor.detach().cpu()
array = tensor.permute(1, 2, 0).numpy()
array = (array * 255).astype(np.uint8)
return Image.fromarray(array)
def numpy_to_pil(img_array):
"""
NumPy image HWC float32 (valeurs entre 0-1) -> PIL Image
"""
img = np.clip(img_array, 0, 1)
img = (img * 255).astype(np.uint8)
return Image.fromarray(img)
SDXL sur images réelles¶
Ici on reprend les images réelles. On aura donc une modification faite à partir du prompt : principalement sur la fourure, beaucoup plus soft et détaillée. Similairement, des yeux vont être ajoutés aux chats les ayant fermés.
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters)) # Choisir les mêmes pour cohérence
for idx in indices:
init_image = tensor_to_pil(x_test[idx])
init_image = init_image.resize((512, 512)) # Resize pour SDXL
prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
result = pipe(prompt=prompt, image=init_image, strength=0.15, guidance_scale=10)
sdxl_image_r = result.images[0]
sdxl_images.append(sdxl_image_r)
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 15.49it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.23it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.32it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.37it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.52it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 17.99it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.37it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.42it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.37it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.09it/s]
n_values_parameters = 10
indices = list(range(n_values_parameters))
plt.figure(figsize=(20, n_values_parameters * 2))
for i, idx in enumerate(indices):
# Convertir les images
original = tensor_to_pil(x_test[idx]) # PyTorch tensor → PIL
noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
autoenc = numpy_to_pil(reconstructed_imgs[idx]) # NumPy → PIL
sdxl = sdxl_images[idx].resize((150, 150)) # déjà PIL, resize si besoin
plt.subplot(n_values_parameters, 4, i * 4 + 1)
plt.imshow(original)
plt.title("Originale")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 4)
plt.imshow(sdxl)
plt.title("SDXL")
plt.axis("off")
plt.tight_layout()
plt.show()
SDXL sur images bruitées¶
Ici on reprend les images bruitées et on va essayer de revenir davantage vers une image normale en utilisant SDXL le plus que possible. Il faut donc avoir un paramètre strength bas pour rester aussi fidèle que possible.
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters)) # Choisir les mêmes pour cohérence
for idx in indices:
init_image = tensor_to_pil(x_test_noisy[idx])
init_image = init_image.resize((512, 512)) # Resize pour SDXL
prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
result = pipe(prompt=prompt, image=init_image, strength=0.05, guidance_scale=9)
sdxl_image = result.images[0]
sdxl_images.append(sdxl_image)
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.99it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 20.62it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 21.05it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 21.28it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.26it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 18.18it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.73it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.81it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s]
n_values_parameters = 10
indices = list(range(n_values_parameters))
plt.figure(figsize=(20, n_values_parameters * 2))
for i, idx in enumerate(indices):
# Convertir les images
original = tensor_to_pil(x_test[idx]) # PyTorch tensor → PIL
noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
autoenc = numpy_to_pil(reconstructed_imgs[idx]) # NumPy → PIL
sdxl = sdxl_images[idx].resize((150, 150)) # déjà PIL, resize si besoin
plt.subplot(n_values_parameters, 4, i * 4 + 1)
plt.imshow(original)
plt.title("Originale")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 2)
plt.imshow(noisy)
plt.title("Bruitée")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 4)
plt.imshow(sdxl)
plt.title("SDXL")
plt.axis("off")
plt.tight_layout()
plt.show()
Les résultats sont très bons mais le denoising n'est pas parfait.
SDXL sur images après DAE - Classique¶
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters)) # Choisir les mêmes pour cohérence
for idx in indices:
dae_image = numpy_to_pil(reconstructed_imgs[idx]) # reconstruite
dae_image = dae_image.resize((512, 512)) # Resize pour SDXL
prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
result = pipe(prompt=prompt, image=dae_image, strength=0.15, guidance_scale=10)
sdxl_from_dae = result.images[0]
sdxl_images.append(sdxl_from_dae)
100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.57it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.57it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.52it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 17.90it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.47it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.62it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 17.90it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.18it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.32it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 18.04it/s]
n_values_parameters = 10
indices = list(range(n_values_parameters))
plt.figure(figsize=(20, n_values_parameters * 2))
for i, idx in enumerate(indices):
# Convertir les images
original = tensor_to_pil(x_test[idx]) # PyTorch tensor → PIL
noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
autoenc = numpy_to_pil(reconstructed_imgs[idx]) # NumPy → PIL
sdxl = sdxl_images[idx].resize((150, 150)) # déjà PIL, resize si besoin
plt.subplot(n_values_parameters, 4, i * 4 + 1)
plt.imshow(original)
plt.title("Originale")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 2)
plt.imshow(noisy)
plt.title("Bruitée")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 3)
plt.imshow(autoenc)
plt.title("DAE")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 4)
plt.imshow(sdxl)
plt.title("SDXL")
plt.axis("off")
plt.tight_layout()
plt.show()
Les images sont très mauvaises car la strength du modèle est basse. Le modèle reste donc assez fidèle à l'image d'entrée. Cependant, cette entrée est elle-même très mauvaise.
SDXL sur images après DAE - UNet¶
Ici on va utiliser SDXL pour ajouter des détails pouvant être manquants en sortie du DAE utilisant UNet.
sdxl_images = []
n_values_parameters = 10
indices = list(range(n_values_parameters)) # Choisir les mêmes pour cohérence
for idx in indices:
dae_unet_image = numpy_to_pil(reconstructed_imgs_unet[idx]) # reconstruite
dae_unet_image = dae_unet_image.resize((512, 512)) # Resize pour SDXL
prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
result = pipe(prompt=prompt, image=dae_unet_image, strength=0.10, guidance_scale=8)
sdxl_from_dae_unet = result.images[0]
sdxl_images.append(sdxl_from_dae_unet)
100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.87it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.73it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.23it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.23it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.59it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.45it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.94it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 17.86it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.30it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.59it/s]
n_values_parameters = 10
indices = list(range(n_values_parameters))
plt.figure(figsize=(20, n_values_parameters * 2))
for i, idx in enumerate(indices):
# Convertir les images
original = tensor_to_pil(x_test[idx]) # PyTorch tensor → PIL
noisy = tensor_to_pil(x_test_noisy[idx]) # déjà PIL
autoenc = numpy_to_pil(reconstructed_imgs_unet[idx]) # NumPy → PIL
sdxl = sdxl_images[idx].resize((150, 150)) # déjà PIL, resize si besoin
plt.subplot(n_values_parameters, 4, i * 4 + 1)
plt.imshow(original)
plt.title("Originale")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 2)
plt.imshow(noisy)
plt.title("Bruitée")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 3)
plt.imshow(autoenc)
plt.title("DAE_UNet")
plt.axis("off")
plt.subplot(n_values_parameters, 4, i * 4 + 4)
plt.imshow(sdxl)
plt.title("SDXL")
plt.axis("off")
plt.tight_layout()
plt.show()
Les images sont ici assez nettes même si un flou reste présent. Le modèle SDXL a réussi à ajouter des détails manquants sur certaines images.
Essayons différentes valeurs de strength et de guidance pour voir l'impact sur la sortie.
sdxl_images = {}
n_values_parameters = 8
indices = list(range(n_values_parameters)) # Choisir les mêmes pour cohérence
strengths = np.linspace(0.05, 0.5, n_values_parameters) # Différents niveaux de force
guidance_scales = np.linspace(1, 10, n_values_parameters) # Différents niveaux de guidance
for s in strengths:
for g in guidance_scales:
dae_unet_image = numpy_to_pil(reconstructed_imgs_unet[idx]) # reconstruite
dae_unet_image = dae_unet_image.resize((512, 512)) # Resize pour SDXL
prompt = "a photo of a clean and realistic domestic cat, DSLR quality, natural lighting, ultra-detailed fur"
result = pipe(prompt=prompt, image=dae_unet_image, strength=s, guidance_scale=g)
sdxl_from_dae_unet = result.images[0]
sdxl_images[(s, g)] = sdxl_from_dae_unet
100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 16.13it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 20.62it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 21.98it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.47it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 22.99it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.26it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 23.53it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 24.15it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.94it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 17.99it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 19.01it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.12it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.59it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.73it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 18.73it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 24.69it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.74it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.98it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 18.10it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 18.26it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.70it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.98it/s] 100%|████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:00<00:00, 17.94it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 25.21it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.65it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.60it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.54it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.47it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.47it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.52it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 17.67it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 24.04it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.40it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.40it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.32it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.42it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.48it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.44it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 15/15 [00:00<00:00, 17.30it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:00<00:00, 25.10it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.44it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.41it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.32it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.36it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.39it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 17.01it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 18/18 [00:01<00:00, 16.92it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:00<00:00, 24.45it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.33it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.16it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.54it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.46it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.56it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.56it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 21/21 [00:01<00:00, 17.07it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:00<00:00, 25.08it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.31it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 16.88it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.14it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 16.86it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.11it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 17.18it/s] 100%|██████████████████████████████████████████████████████████████████████████████████| 25/25 [00:01<00:00, 16.90it/s]
fig, axs = plt.subplots(n_values_parameters, n_values_parameters, figsize=(25, 20))
for i, s in enumerate(strengths):
for j, g in enumerate(guidance_scales):
ax = axs[i, j]
sdxl_image = sdxl_images[(s, g)].resize((150, 150)) # Resize pour affichage
ax.imshow(sdxl_image)
ax.set_title(f"Strength: {s:.2f}, Guidance: {g:.2f}")
ax.axis("off")
plt.tight_layout()
plt.show()
On peut remarquer qu'une strength élevée va faire que le modèle crée des images plus proches du prompt uniquement, en essayant de créer une image de toute pièce, tandis qu'une strength faible va essayer de rester fidèle à l'image d'entrée.
Finalement...¶
On obtient des résultats intéressants via le UNet et SDXL. On pourrait encore altérer les paramètres et les configurations pour tendre vers des résultats plus intéressants. De même on pourrait essayer d'augmenter la taille de l'espace latent de l'autoencoder ou encore modifier les hyperparamètres des couches de convolution pour obtenir des images moins bruitées en sortie.
Mais au final ça reste des chats. Et même si le résultat n'est pas des plus satisfaisants, le chemin l'a été.
DISCLAIMER !!! Regarder les images de chats qui suivent pourrait être troublant pour certains. Si vous jugez ne pas être capable de subir ce CHATiment, veuillez ne pas dépasser la ligne suivante.
Voici quelques chats générés dont on se souviendra (vous de même par la même occasion... si vous osez descendre dans le document):
Hall of Shame ฅ^•ﻌ•^ฅ¶
from IPython.display import Image, display, Markdown
import random
titles = [
"Chatastrophe Numérique",
"Miaul-heureusement raté",
"Erreur Cat cent Cat",
"Paw-norama de l’échec"
]
image_paths = ["./img/A.png", "./img/B.png", "./img/C.png", "./img/D.png", "./img/E.png"]
for path in image_paths:
title = random.choice(titles)
display(Markdown(f"### 🐾 *{title}*"))
display(Image(path))
HTML¶
!jupyter nbconvert --to html mon_notebook.ipynb --output DeepLearning_KAMIONKA_BENALI.html